// <copyright file="ModifyFile.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation.  All rights reserved.
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY 
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
// </copyright>
// <author>Andy Reeves</author>
// <email>andyr</email>
// <date>2004-03-23</date>
// <summary>Reads in an XML file and modifies the contents based on the XPath and values
// to replace in the file.
// </summary>

namespace Microsoft.Sdc.Tasks.Xml
{
    using System;
    using System.Xml;
    using System.IO;
    using System.Text.RegularExpressions;
    using Microsoft.Build.Framework;

    /// <summary>
    /// Reads in an XML file and modifies the contents based on the XPath and values
    /// to replace in the file.
    /// </summary>
    /// <remarks>
    /// <code><![CDATA[
    /// <Xml.ModifyFile
    ///             Path="path"
    ///             AttributeName="attributeName"
    ///             Force="force"
    ///             Namespace="namespace"
    ///             NewValue="newValue"
    ///             XPath="xPath" 
    ///             RegularExpression="regularExpression" >
    ///             ShowMatches="showMatchOptionValue" >
    /// </Xml.ModifyFile>
    /// ]]></code>
    /// <para>where:</para>
    /// <para><i>path (Required)</i></para>
    /// <para>The full path and filename of the xml file to be modified.</para>
    /// <para><i>attributeName</i></para>
    /// <para>The name of the xml attribute to be modified.</para>
    /// <para><i>force</i></para>
    /// <para>If set to true, this will modify the xml file even if it is read-only. Set to false to ignore read-only files. Defaults to false.</para>
    /// <para><i>namespace</i></para>
    /// <para>An array of TaskItems specifiying "Prefix" and "Uri" attributes for use with the specified <i>xPath</i>.</para>
    /// <para><i>newValue</i></para>
    /// <para>The new value for the specified attribute.</para>
    /// <para><i>xPath</i></para>
    /// <para>The XPath to the nodes containing the specified attribute. If the XPath expression includes a prefix, the prefix and namespace URI pair must be added to via the <i>namespace</i> property.</para>
    /// <para><i>regularExpression</i></para>
    /// <para>This property allows a regular expression to be provided in preference to an XPath.</para>
    /// <para><i>showMatchOptionvalue</i></para>
    /// <para>Specifes whether XPath replacements are logged. Valid values are No, Yes, Masked, and Secure.
    /// If not specified then this options defaults to No. The Yes option displays the replacement text
    /// while the Masked and Secure options obscure the replacement text. These options are useful when
    /// the replacement text is sensitive data such as passwords. The Masked option replaces each character
    /// of the replacemen text with an asterisk character. The Secure option always displays 8 asterisks
    /// for the replacement text.
    /// </para>
    /// </remarks>
    /// <example>
    /// <code><![CDATA[
    /// <Project>
    ///     <Target Name="Test" >
    ///         <Xml.ModifyFile
    ///             Path="C:\SourceFiles\file1.xml"
    ///             XPath="/TestNodes/TestNode"
    ///             AttributeName="NodeID"
    ///             NewValue="MODIFIED"
    ///             ShowMatches="Yes"
    ///         </Xml.ModifyFile> 
    ///     </Target>
    /// </Project>
    /// ]]></code>    
    /// </example>
    public class ModifyFile : TaskBase
    {
        /// <summary>
        /// The action to be performed.
        /// </summary>
        private ModifyActionOptionValue action = ModifyActionOptionValue.Replace;

        private string path = String.Empty;
        private bool force = false;
        private bool treatNewValueAsXml = false;

        private string xPath = String.Empty;
        private string regularExpression = String.Empty;
        private string newValue = String.Empty;
        private string attributeName = String.Empty;
        private ShowMatchOptionValue showMatches = ShowMatchOptionValue.No;
        private ITaskItem[] taskitemInput = null;

        /// <summary>
        /// The action to be performed.
        /// </summary>
        public string Action
        {
            get
            {
                return this.action.ToString();
            }

            set
            {
                if (Enum.IsDefined(typeof(ModifyActionOptionValue), value) == false)
                {
                    throw new TaskException(string.Format(TaskResources.GetString("Xml.ModifyFile.InvalidAction"), value));
                }

                this.action = (ModifyActionOptionValue)Enum.Parse(typeof(ModifyActionOptionValue), value, true);
            }
        }

        /// <summary>
        /// The full path and filename of the xml file to be modified.
        /// </summary>
        /// <value>The full path and filename of the xml file to be modified.</value>
        [Required]
        public string Path
        {
            get
            {
                return (this.path ?? String.Empty);
            }

            set
            {
                this.path = value;

                return;
            }
        }

        /// <summary>
        /// An array of TaskItems specifiying "Prefix" and "Uri" attributes for use with the specified <i>xPath</i>.
        /// </summary>
        /// <value>An array of TaskItems specifiying "Prefix" and "Uri" attributes for use with the specified <i>xPath</i>.</value>
        public ITaskItem[] Namespace
        {
            get
            {
                return this.taskitemInput;
            }

            set
            {
                this.taskitemInput = value;

                return;
            }
        }

        /// <summary>
        /// The XPath to the nodes containing the specified attribute.
        /// </summary>
        /// <value>The XPath to the nodes containing the specified attribute.</value>
        public string XPath
        {
            get
            {
                return (this.xPath ?? String.Empty);
            }

            set
            {
                this.xPath = value;

                return;
            }
        }

        /// <summary>
        /// This property allows a regular expression to be provided in preference to an XPath.
        /// </summary>
        /// <value>This property allows a regular expression to be provided in preference to an XPath.</value>
        public string RegularExpression
        {
            get
            {
                return (this.regularExpression ?? String.Empty);
            }

            set
            {
                this.regularExpression = value;

                return;
            }
        }

        /// <summary>
        /// Gets or Sets The new value for the specified attribute.
        /// </summary>
        /// <value>The new value for the specified attribute.</value>
        public string NewValue
        {
            get
            {
                return (this.newValue ?? String.Empty);
            }

            set
            {
                this.newValue = value;

                return;
            }
        }

        /// <summary>
        /// The name of the xml attribute to be modified.
        /// </summary>
        /// <value>The name of the xml attribute to be modified.</value>
        public string AttributeName
        {
            get
            {
                return (this.attributeName ?? String.Empty);
            }

            set
            {
                this.attributeName = value;

                return;
            }
        }

        /// <summary>
        /// If set to true, this will modify the xml file even if it is read-only.
        /// </summary>
        /// <value>If set to true, this will modify the xml file even if it is read-only. Set to false to ignore read-only files. Defaults to false.</value>
        public bool Force
        {
            get
            {
                return this.force;
            }

            set
            {
                this.force = value;
            }
        }

        /// <summary>
        /// If set to true, this will insert newValue as Xml rather than as text. Defaults to false.
        /// </summary>
        /// <value>If set to true, this will insert newValue as Xml rather than as text. Defaults to false.</value>
        public bool TreatNewValueAsXml
        {
            get
            {
                return this.treatNewValueAsXml;
            }

            set
            {
                this.treatNewValueAsXml = value;
            }
        }

        /// <summary>
        /// Gets or Sets whether XPath matches and replacements should be logged. Valid values
        /// are No (default), Yes, and Secure. When Secure is in effect the replacement text
        /// is obscured using '*' characters to hide sensitive data.
        /// </summary>
        public string ShowMatches
        {
            get
            {
                return this.showMatches.ToString();
            }

            set
            {
                if (!Enum.IsDefined(typeof(ShowMatchOptionValue), value))
                {
                    throw new TaskException(string.Format(TaskResources.GetString("Xml.ModifyFile.ShowMatchesArgException"), value));
                }

                this.showMatches = (ShowMatchOptionValue)Enum.Parse(typeof(ShowMatchOptionValue), value, true);
            }
        }

        /// <summary>
        /// Performs the action of this task.
        /// </summary>
        protected override void InternalExecute()
        {
            #region Execute code

            Log.LogMessageFromResources("File.UpdateContents", this.Path);

            if (System.IO.File.Exists(this.Path))
            {
                // If the XPath is specified, search the document for the elements that have that
                // pattern
                if (this.XPath.Length > 0)
                {
                    XmlDocument doc = new XmlDocument();
                    string fileContents;
                    using (StreamReader sr = new StreamReader(this.Path))
                    {
                        fileContents = sr.ReadToEnd();
                        sr.Close();
                    }

                    doc.LoadXml(fileContents);

                    XmlNamespaceManager nsmgrFile = new XmlNamespaceManager(doc.NameTable);

                    // Take all the values out of ITaskItem collection, and put it
                    // into a hash table for the execute method
                    if (null != this.taskitemInput)
                    {
                        foreach (ITaskItem item in this.taskitemInput)
                        {
                            string prefix = item.GetMetadata("Prefix");
                            string uri = item.GetMetadata("Uri");

                            try
                            {
                                nsmgrFile.AddNamespace(prefix, uri);
                            }
                            catch (Exception e)
                            {
                                Log.LogWarningFromResources("File.ProblemAddingNamespace", prefix, uri, e.Message);
                            }
                        }
                    }

                    // Iterate through the document, updating the values
                    XmlNodeList nodeList = doc.SelectNodes(this.XPath, nsmgrFile);
                    foreach (XmlNode node in nodeList)
                    {
                        string newValueForAction;
                        string oldValue;

                        if (this.AttributeName.Length > 0)
                        {
                            XmlAttribute attr = node.Attributes[this.AttributeName];
                            if (attr == null)
                            {
                                oldValue = "";
                                attr = doc.CreateAttribute(this.AttributeName);
                                node.Attributes.Append(attr);
                                newValueForAction = this.BuildNewValueForAction(oldValue);
                                node.Attributes[attributeName].Value = newValueForAction;
                            }
                            else
                            {
                                oldValue = attr.Value;
                                newValueForAction = this.BuildNewValueForAction(oldValue);
                                node.Attributes[attributeName].Value = newValueForAction;
                            }
                        }
                        else
                        {
                            if (this.TreatNewValueAsXml)
                            {
                                oldValue = node.InnerXml;
                                newValueForAction = this.BuildNewValueForAction(oldValue);
                                node.InnerXml = newValueForAction;
                            }
                            else
                            {
                                oldValue = node.InnerText;
                                newValueForAction = this.BuildNewValueForAction(oldValue);
                                node.InnerText = newValueForAction;
                            }
                        }

                        this.LogMatches(this.XPath, oldValue, this.NewValue);
                    }

                    // Force a write on the file if specified
                    if (this.Force)
                    {
                        FileInfo fi = new FileInfo(this.Path);
                        fi.Attributes = FileAttributes.Normal;
                    }

                    // Save the results
                    using (StreamWriter sw = new StreamWriter(this.Path))
                    {
                        doc.Save(sw);
                        sw.Close();
                    }
                }
                else if (this.RegularExpression.Length > 0)
                {
                    // Now everyone should use the File.RegEx task instead
                    // Warn them for a while
                    Log.LogWarning("*** Please use File.RegEx task instead of Xml.ModifyFile for replacing text ***");                    

                    // The replace action is the only action supported for a regular expression
                    if (this.action != ModifyActionOptionValue.Replace)
                    {
                        throw new TaskException("Xml.ModifyFile.InvalidAction", this.action.ToString());
                    }

                    // Load contents into fileContents
                    FileInfo fi = new FileInfo(this.Path);
                    FileAttributes originalAttributes = fi.Attributes;

                    string fileContents;
                    using (StreamReader textFileReader = new StreamReader(this.Path))
                    {
                        fileContents = textFileReader.ReadToEnd();
                        textFileReader.Close();
                    }

                    // Replace matched values in fileContents
                    Regex regex = new Regex(this.RegularExpression);
                    fileContents = regex.Replace(fileContents, this.NewValue);

                    // Set file attributes so that we can overwrite if necessary
                    if (this.Force)
                    {
                        fi.Attributes = FileAttributes.Normal;
                    }

                    // Write new contents to file
                    using (StreamWriter textFileWriter = new StreamWriter(this.Path))
                    {
                        textFileWriter.Write(fileContents);
                        textFileWriter.Close();
                    }

                    if (this.Force)
                    {
                        fi.Attributes = originalAttributes;
                    }
                }
                else
                {
                    throw new TaskException("File.XPathOrRegularExpressionRequired");
                }
            }
            else
            {
                throw new TaskException("File.NotFound", this.Path);
            }
        }

        /// <summary>
        /// Builds the new value appropriate for the action to be performed.
        /// </summary>
        /// <param name="oldValue">The old value.</param>
        /// <returns>The new value.</returns>
        private string BuildNewValueForAction(string oldValue)
        {
            string newValueForAction;

            switch (this.action)
            {
                case ModifyActionOptionValue.Append:
                    newValueForAction = oldValue + this.newValue;
                    break;

                case ModifyActionOptionValue.Replace:
                    newValueForAction = this.newValue;
                    break;

                default:
                    throw new TaskException("Xml.ModifyFile.InvalidAction", this.action.ToString());
            }

            return newValueForAction;
        }

        /// <summary>
        /// Display the matched XPath and replacement text according to the ShowMatches option.
        /// </summary>
        /// <param name="xmlPath">The XML path.</param>
        /// <param name="oldValue">The old value.</param>
        /// <param name="newVal">The new val.</param>
        private void LogMatches(string xmlPath, string oldValue, string newVal)
        {
            // Get the appropriate description for the action that was performed
            string resourceName;
            switch (this.action)
            {
                case ModifyActionOptionValue.Append:
                    resourceName = "Xml.ModifyFile.XPathMatchedActionAppend";
                    break;

                case ModifyActionOptionValue.Replace:
                    resourceName = "Xml.ModifyFile.XPathMatched";
                    break;

                default:
                    throw new TaskException("Xml.ModifyFile.InvalidAction", this.action.ToString());
            }

            // Log the action that was performed
            switch (this.showMatches)
            {
                case ShowMatchOptionValue.No:
                    break;

                case ShowMatchOptionValue.Yes:
                    Log.LogMessageFromResources(resourceName, xmlPath, oldValue, newVal);
                    break;

                case ShowMatchOptionValue.Masked:
                    Log.LogMessageFromResources(resourceName, xmlPath, oldValue, new string('*', newVal.Length));
                    break;

                case ShowMatchOptionValue.Secure:
                    Log.LogMessageFromResources(resourceName, xmlPath, oldValue, new string('*', 8));
                    break;
            }
        }
        #endregion
    }
}
